/****************************************************************************
 * Copyright (c) 2010 Torkild U. Resheim
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Torkild U. Resheim  - initial API and implementation
 *****************************************************************************/
package no.resheim.buildmonitor.core.data;

import java.net.URI;
import java.util.Observable;

import org.eclipse.core.resources.IStorage;

/**
 * Represents a build &quot;job&quot;. Subclasses must implement methods for
 * obtaining {@link Build} information.
 * 
 * @author Torkild Ulvøy Resheim
 * @since 1.0
 */
public abstract class Job extends Observable implements Comparable<Job>,
		IChangeReporter {
	/**
	 * The general health of the build job.
	 */
	public enum Health {
		/** Last build failed */
		ERROR,
		/** Last build tests failed */
		FAILED,
		/** Last build succeeded and tests did not fail */
		GOOD,
		/** Last build was aborted */
		ABORTED,
		/** The job has been disabled */
		DISABLED,
		/** The health state is not known */
		UNKNOWN
	}

	/** Whether or not the job is active */
	private boolean active;

	/** Whether or not the job can be used to start new builds */
	private boolean buildable;

	/** The description of the job */
	private String description;

	/** The display name of the job */
	private String displayName;

	/** The first build number */
	private int firstBuildNumber;

	/** The health of the job */
	private Health health;

	/** A short description of the health */
	private String healthReport;

	/** Whether or not the job is in the queue */
	private boolean inQueue;

	/** The number of the last started build */
	private int lastStartedBuildNumber;

	/** The number of the last completed build */
	private int lastCompletedBuildNumber;

	/** The number of the last failed build */
	private int lastFailedBuildNumber;

	/** The number of the last stable build */
	private int lastStableBuildNumber;

	/** The number of the last successful build */
	private int lastSuccessfulBuildNumber;

	/** The name of the build job */
	private String name;

	private String changeDescription;

	/**
	 * Returns a description or summary of the last change that occurred in the
	 * job. For instance if a new build has been queued, the value returned from
	 * this method will say something like: &quot;build #xx has been
	 * queued&quot;.
	 * 
	 * @return a description of the change
	 */
	public String getChangeDescription() {
		return changeDescription;
	}

	/**
	 * Sets the description of the last change.
	 * 
	 * @param changeDescription
	 *            a short description
	 */
	public void setChangeDescription(String changeDescription) {
		this.changeDescription = changeDescription;
	}

	/** The URI of the build job */
	private final URI uri;

	protected Job(URI uri) {
		if (uri == null) {
			throw new IllegalArgumentException("Job URI cannot be NULL.");
		}
		this.uri = uri;
	}

	/**
	 * Issue a request for a new build of this job. It is up to the actual
	 * implementation to handle how this is done.
	 * 
	 * @see #terminate()
	 */
	public abstract void build();

	/**
	 * Jobs with bad health are of higher significance than jobs with a good
	 * health.
	 */
	public int compareTo(Job o) {
		int s = getHealth().compareTo(o.getHealth());
		if (s == 0) {
			return getDisplayName().compareTo(o.getDisplayName());
		}
		return s;
	}

	/**
	 * Returns the build identified by the specified number. It is up to the
	 * actual implementation to handle how this is done.
	 * 
	 * @param number
	 *            the build number
	 * @return the build
	 */
	public abstract Build getBuild(int number);

	/**
	 * Returns the description of this build job. This is a short textual
	 * presentation suitable for user consumption.
	 * 
	 * @return the job description
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Returns the display name of the job.
	 * 
	 * @return the display name of the job.
	 */
	public String getDisplayName() {
		return displayName;
	}

	/**
	 * Returns the estimated number of milliseconds to complete the job in
	 * progress or -1 if there is not enough information for calculation
	 * available. -1 will also be returned if the last started job is done
	 * building.
	 * 
	 * @return the estimated completion of the job
	 */
	public long getEstimatedCompletion() {
		if (getLastStartedBuild() == null || getLastCompletedBuild() == null
				|| !getLastStartedBuild().isBuilding()) {
			return -1;
		}
		// TODO: Record whether or not a build was completed and use averages
		return getLastCompletedBuild().getDuration()
				- getLastStartedBuild().getDuration();
	}

	/**
	 * Returns the number of the first known build.
	 * 
	 * @return the first known build number
	 */
	public int getFirstBuildNumber() {
		return firstBuildNumber;
	}

	/**
	 * Returns the current health of the job.
	 * 
	 * @return the current health
	 */
	public Health getHealth() {
		return health;
	}

	/**
	 * Returns a short (one line) description of the job health.
	 * 
	 * @return the job health description
	 */
	public String getHealthReport() {
		return healthReport;
	}

	/**
	 * Returns the last build either started or finished.
	 * 
	 * @return the last build
	 */
	public Build getLastStartedBuild() {
		return getBuild(lastStartedBuildNumber);
	}

	/**
	 * Returns the number of the last build.
	 * 
	 * @return the number of the last build
	 */
	public int getLastStartedBuildNumber() {
		return lastStartedBuildNumber;
	}

	/**
	 * Returns the last build completed.
	 * 
	 * @return the last build completed
	 */
	private Build getLastCompletedBuild() {
		return getBuild(lastCompletedBuildNumber);
	}

	/**
	 * Returns the number of the last build that was completed.
	 * 
	 * @return the last completed build number
	 */
	public int getLastCompletedBuildNumber() {
		return lastCompletedBuildNumber;
	}

	/**
	 * Return the number of the latest build that was completed but with
	 * failures.
	 * 
	 * @return the latest failed build number
	 */
	public int getLastFailedBuildNumber() {
		return lastFailedBuildNumber;
	}

	/**
	 * Returns the number of the latest build that was completed without any
	 * kind of failures.
	 * 
	 * @return the latest stable build number
	 */
	public int getLastStableBuildNumber() {
		return lastStableBuildNumber;
	}

	/**
	 * Returns the number of the latest build that was completed without error.
	 * Note that it test may still have failed.
	 * 
	 * @return the latest successful build number
	 */
	public int getLastSuccessfulBuildNumber() {
		return lastSuccessfulBuildNumber;
	}

	/**
	 * Returns the log output from the latest build. Subclasses must implement
	 * this method and is hence allowed to perform whatever actions required to
	 * obtain the log text.
	 */
	public abstract IStorage getLog();

	/**
	 * Returns the log output from the specified build.
	 */
	public abstract IStorage getLog(int build);

	/**
	 * Returns the name of the job. This may differ from the <i>display
	 * name</i>.
	 * 
	 * @return the job name
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns the URI of the job.
	 * 
	 * @return the job URI
	 */
	public URI getUri() {
		return uri;
	}

	/**
	 * Indicates whether or not the job is active, that is if a build is in
	 * progress.
	 * 
	 * @return <code>true</code> if the job is currently building
	 */
	public boolean isActive() {
		return active;
	}

	/**
	 * Indicates whether or not a new build can be issued.
	 * 
	 * @return <code>true</code> if builds can be performed
	 */
	public boolean isBuildable() {
		return buildable;
	}

	/**
	 * Indicates whether or not a build us queued.
	 * 
	 * @return <code>true</code> if a build is queued
	 */
	public boolean isInQueue() {
		return inQueue;
	}

	/**
	 * Sets the active flag of the job. If the value given is different than the
	 * original the changed flag will be set.
	 * 
	 * @param active
	 *            the new active flag
	 */
	protected void setActive(boolean active) {
		if (this.active != active)
			setChanged();
		this.active = active;
	}

	/**
	 * Sets the buildable flag of the job. If the value given is different than
	 * the original the changed flag will be set.
	 * 
	 * @param buildable
	 *            the new buildable flag
	 */
	protected void setBuildable(boolean buildable) {
		if (this.buildable != buildable)
			setChanged();
		this.buildable = buildable;
	}

	/**
	 * Sets the description of the build job. If the value given is different
	 * than the original the changed flag will be set.
	 * 
	 * @param description
	 *            the new description
	 */
	protected void setDescription(String description) {
		if (this.description == null || !this.description.equals(description))
			setChanged();
		this.description = description;
	}

	/**
	 * Sets the display name of this job. This should be a short concise text
	 * suitable for display in the user interface. If the value given is
	 * different than the original the changed flag will be set.
	 * 
	 * @param name
	 *            the display name of the job
	 */
	protected void setDisplayName(String name) {
		if (this.displayName == null || !this.displayName.equals(name))
			setChanged();
		this.displayName = name;
	}

	/**
	 * Set the number of the first build of this job. If the value given is
	 * different than the original the changed flag will be set.
	 * 
	 * @param number
	 *            the number of the first build
	 */
	protected void setFirstBuildNumber(int number) {
		if (this.firstBuildNumber != number)
			setChanged();
		this.firstBuildNumber = number;
	}

	protected void setHealth(Health health) {
		this.health = health;
	}

	/**
	 * Sets a string describing the health of the build. We assume the new value
	 * is never <code>null</code>.If the value given is different than the
	 * original the changed flag will be set.
	 * 
	 * @param healthReport
	 *            the new health description
	 */
	protected void setHealthReport(String healthReport) {
		if (this.healthReport == null
				|| !this.healthReport.equals(healthReport)) {
			setChanged();
		}
		this.healthReport = healthReport;
	}

	/**
	 * Sets the flag indicating whether or not this build is in the queue. If
	 * the value given is different than the original the changed flag will be
	 * set.
	 * 
	 * @param inQueue
	 *            whether or not the build is in queue
	 */
	protected void setInQueue(boolean inQueue) {
		if (this.inQueue != inQueue)
			setChanged();
		this.inQueue = inQueue;
	}

	/**
	 * Sets the number of the last build started. If the value given is
	 * different than the original the changed flag will be set.
	 * 
	 * @param number
	 *            the number to set
	 */
	protected void setLastStartedBuildNumber(int number) {
		if (this.lastStartedBuildNumber != number)
			setChanged();
		this.lastStartedBuildNumber = number;
	}

	/**
	 * Sets the number of the last completed build. If the value given is
	 * different than the original the changed flag will be set.
	 * 
	 * @param number
	 *            the number to set
	 */
	protected void setLastCompletedBuildNumber(int number) {
		if (this.lastCompletedBuildNumber != number) {
			setChanged();
			this.lastCompletedBuildNumber = number;
		}
	}

	/**
	 * Sets the number of the last failed build. If the value given is different
	 * than the original the changed flag will be set.
	 * 
	 * @param number
	 */
	protected void setLastFailedBuildNumber(int number) {
		if (this.lastFailedBuildNumber != number)
			setChanged();
		this.lastFailedBuildNumber = number;
	}

	/**
	 * Sets the number of the last stable build. If the value given is different
	 * than the original the changed flag will be set.
	 * 
	 * @param number
	 *            the number to test
	 */
	protected void setLastStableBuildNumber(int number) {
		if (this.lastStableBuildNumber != number)
			setChanged();
		this.lastStableBuildNumber = number;
	}

	/**
	 * Sets the number of the last successful build. If the value given is
	 * different than the original the changed flag will be set.
	 * 
	 * @param number
	 *            the number to set
	 */
	protected void setLastSuccessfulBuildNumber(int number) {
		if (this.lastSuccessfulBuildNumber != number)
			setChanged();
		this.lastSuccessfulBuildNumber = number;
	}

	/**
	 * Sets the name of the build job. We assume the new value is never
	 * <code>null</code>.If the value given is different than the original the
	 * changed flag will be set.
	 * 
	 * @param name
	 *            the name to set
	 */
	protected void setName(String name) {
		if (this.name == null || !this.name.equals(name))
			setChanged();
		this.name = name;
	}

	/**
	 * Terminates the build in progress. It is up to the actual implementation
	 * to handle how this is done.
	 * 
	 * @see #build()
	 */
	public abstract void terminate();

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(displayName);
		if (isActive()) {
			sb.append(" (building)");
		}
		return sb.toString();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + uri.hashCode();
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Job)) {
			return false;
		}
		if (!uri.equals(((Job) obj).uri)) {
			return false;
		}
		return true;
	}
}
